package com.example.framework.security.core.util;

import com.example.framework.common.exception.ErrorCode;
import com.example.framework.common.exception.ServiceException;
import com.example.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.example.framework.common.util.json.JsonUtils;
import com.example.framework.redis.core.util.RedisUtils;
import com.example.framework.security.config.SecurityProperties;
import com.example.framework.security.core.LoginUser;
import com.example.framework.security.core.dto.JwtTokenRespDto;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;

import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@RequiredArgsConstructor
public class JwtTokenUtils {

    private static final String JWT_CACHE_PREFIX = "common:jwt:";
    private static final String ACCESS_TOKEN_PREFIX = JWT_CACHE_PREFIX + "access_token:";
    private static final String REFRESH_TOKEN_PREFIX = JWT_CACHE_PREFIX + "refresh_token:";
    private static final String USER_INFO_PREFIX = JWT_CACHE_PREFIX + "user_info:";
    private static final String DEVICE_IDS_PREFIX = JWT_CACHE_PREFIX + "device_ids:";

    private final SecurityProperties securityProperties;

    /**
     * 生成 jwt token，并存储用户信息至Redis
     *
     * @param loginUser 用户信息，用来存储在Redis，如果要存储其他字段可以继承此类
     * @return 令token牌
     */
    public JwtTokenRespDto generateTokenAndRefreshToken(LoginUser loginUser) {
        JwtTokenRespDto jwtTokenRespDto = buildToken(loginUser);
        cacheToken(jwtTokenRespDto.getAccessToken(), jwtTokenRespDto.getRefreshToken(), loginUser);
        return jwtTokenRespDto;
    }

    @SuppressWarnings("unchecked")
    private JwtTokenRespDto buildToken(LoginUser loginUser) {
        String userId = loginUser.getUsername();

        // 生成 Access Token，过期时间较短，储存用户信息权限等
        String accessToken = Jwts.builder()
                .setSubject(userId)
                .setIssuedAt(new Date())
//                .setExpiration(new Date(System.currentTimeMillis() + securityProperties.getAccessTokenValiditySeconds() * 1000)) // 需要心跳中在不改变token的前提下实现续期，故不设置jwt的过期时间
                .addClaims(JsonUtils.convert(loginUser, Map.class))
                .signWith(SignatureAlgorithm.HS512, securityProperties.getSecret())
                .compact();
        // 生成 Refresh Token，过期时间较长，不储存额外信息，只储存用户id
        String refreshToken = Jwts.builder()
                .setSubject(userId)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + securityProperties.getRefreshTokenValiditySeconds() * 1000))
                .signWith(SignatureAlgorithm.HS512, securityProperties.getSecret())
                .compact();
        return new JwtTokenRespDto()
                .setAccessToken(accessToken)
                .setAccessTokenExpiresIn(securityProperties.getAccessTokenValiditySeconds())
                .setRefreshToken(refreshToken)
                .setRefreshTokenExpiresIn(securityProperties.getRefreshTokenValiditySeconds());
    }

    private void cacheToken(String accessToken, String refreshToken, LoginUser loginUser) {
        String userId = loginUser.getUsername();

        RedisUtils.set(USER_INFO_PREFIX + userId, loginUser, securityProperties.getRefreshTokenValiditySeconds(), TimeUnit.SECONDS);
        RedisUtils.set(ACCESS_TOKEN_PREFIX + userId, accessToken, securityProperties.getAccessTokenValiditySeconds(), TimeUnit.SECONDS);
        RedisUtils.set(REFRESH_TOKEN_PREFIX + userId, refreshToken, securityProperties.getRefreshTokenValiditySeconds(), TimeUnit.SECONDS);
        RedisUtils.sAdd(DEVICE_IDS_PREFIX + userId, loginUser.getDeviceId());
    }

    public void renewAccessToken(String userId) {
        if (!securityProperties.isAccessTokenRenewEnable()) {
            return;
        }
        String redisKey = ACCESS_TOKEN_PREFIX + userId;
        Long expire = RedisUtils.getExpire(redisKey, TimeUnit.SECONDS);
        if (expire > 0 && expire <= securityProperties.getAccessTokenRenewLeftSeconds()) {
            RedisUtils.expire(redisKey, securityProperties.getAccessTokenRenewSeconds(), TimeUnit.SECONDS); // 续期
        }
    }

    public void updateUserInfo(LoginUser loginUser) {
        String redisKey = USER_INFO_PREFIX + loginUser.getUsername();
        RedisUtils.set(redisKey, loginUser, RedisUtils.getExpire(redisKey, TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
    }

    public LoginUser getUserInfoFromCacheByToken(String token) {
        String userId = getUserIdFromToken(token);
        return getUserInfoFromCacheByUserId(userId);
    }

    public LoginUser getUserInfoFromCacheByUserId(String userId) {
        String redisKey = USER_INFO_PREFIX + userId;
        return RedisUtils.get(redisKey, LoginUser.class);
    }

    /**
     * 从令牌中获取用户id
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUserIdFromToken(String token) {
        return getClaimsFromToken(token).getSubject();
    }

    public String getDeviceIdFromToken(String token) {
        Claims body = Jwts.parser().setSigningKey(securityProperties.getSecret()).parseClaimsJws(token).getBody();
        return (String) body.get("deviceId");
    }

    /**
     * 从令牌中获取数据声明,验证 JWT 签名
     *
     * @param token 令牌
     * @return 数据声明
     */
    @NotNull
    private Claims getClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(securityProperties.getSecret()).parseClaimsJws(token).getBody();
    }

    public void checkAccessToken(String accessToken) {
        checkTokenExpired(accessToken,
                () -> GlobalErrorCodeConstants.ACCESS_TOKEN_EXPIRED,
                () -> GlobalErrorCodeConstants.INVALID_ACCESS_TOKEN);

        String userId = getUserIdFromToken(accessToken);
        String tokenFromCache = RedisUtils.getString(ACCESS_TOKEN_PREFIX + userId);
        if (Objects.isNull(tokenFromCache)) {
            // 被踢下线
            Boolean isMember = RedisUtils.sIsMember(DEVICE_IDS_PREFIX + userId, getDeviceIdFromToken(accessToken));
            if (!Boolean.TRUE.equals(isMember)) {
                throw ServiceException.of(GlobalErrorCodeConstants.BEEN_KICKED_OFFLINE);
            }

            // 过期了
            String refreshToken = RedisUtils.getString(REFRESH_TOKEN_PREFIX + userId);
            if (Objects.isNull(refreshToken)) {
                // 双token都没了，长时间未登录，要重新登录
                throw ServiceException.warn(GlobalErrorCodeConstants.TOKEN_EXPIRED);
            }
            throw ServiceException.warn(GlobalErrorCodeConstants.ACCESS_TOKEN_EXPIRED);
        }
        if (!tokenFromCache.equals(accessToken)) {
            throw ServiceException.warn(GlobalErrorCodeConstants.LOGGED_IN_ON_ANOTHER_DEVICE);
        }
    }

    public void checkRefreshToken(String refreshToken) {
        checkTokenExpired(refreshToken,
                () -> GlobalErrorCodeConstants.REFRESH_TOKEN_EXPIRED,
                () -> GlobalErrorCodeConstants.INVALID_REFRESH_TOKEN);

        String userId = getUserIdFromToken(refreshToken);
        String tokenFromCache = RedisUtils.getString(REFRESH_TOKEN_PREFIX + userId);
        if (Objects.isNull(tokenFromCache)) {
            throw ServiceException.warn(GlobalErrorCodeConstants.REFRESH_TOKEN_EXPIRED);
        }
        if (!tokenFromCache.equals(refreshToken)) {
            throw ServiceException.warn(GlobalErrorCodeConstants.REFRESH_TOKEN_DELETED);
        }
    }

    private void checkTokenExpired(String token, Supplier<ErrorCode> expiredErrorCodeSupplier, Supplier<ErrorCode> parseExceptionErrorCodeSupplier) {
        try {
            getClaimsFromToken(token);
        } catch (ExpiredJwtException e) {
            // 过期会抛出异常
            throw ServiceException.warn(expiredErrorCodeSupplier.get());
        } catch (Exception e) {
            throw ServiceException.warn(parseExceptionErrorCodeSupplier.get());
        }
    }

    public void removeToken(String userId) {
        RedisUtils.delete(USER_INFO_PREFIX + userId);
        RedisUtils.delete(ACCESS_TOKEN_PREFIX + userId);
        RedisUtils.delete(REFRESH_TOKEN_PREFIX + userId);
        RedisUtils.delete(DEVICE_IDS_PREFIX + userId);
    }

    public void removeToken(String userId, String deviceId) {
        RedisUtils.delete(USER_INFO_PREFIX + userId);
        RedisUtils.delete(ACCESS_TOKEN_PREFIX + userId);
        RedisUtils.delete(REFRESH_TOKEN_PREFIX + userId);
        RedisUtils.sRemove(DEVICE_IDS_PREFIX + userId, deviceId);
    }

}